﻿using System.Text;
using System;
using System.IO;
using System.Linq;
using System.Security;
using System.Globalization;

namespace CodeBetter.Extensions
{
   // todo exception raising in desired cases
   // todo it is not obvious lowercase is the default we might need a app.config
   // todo same goes for little endian usage (there is also middle(mixed) endianess
   // todo there is also 8 or 16 byte alignment
   // todo als strings of all kinds of encodings could be added?
   // todo hexing should move to usable Encode/Decoder
   // todo nullable type should have a default that can be passed

   internal class HexString // ? todo could this one be public ?
   {
      private byte[] bytes;
      private bool upperCase;
      private static BinHexEncoding coder;

      static HexString()
      {
        coder = new BinHexEncoding();
      }

      public HexString(byte[] source)
      {
         bytes = source;
      }

      public HexString(byte[] source, HexOptions options)
      {
         this.upperCase = options.IsUpperCase();
         bytes = source;
      }

      public HexString(string source)
      {
         bytes = coder.GetBytes(source);
      }

      public static implicit operator string(HexString value)
      {
         string result = coder.GetString(value.bytes);
         if (result != null)
         {
            if (value.upperCase)
               return result;
            return result.ToLower();
         }
         else
            return null;
      }

      //public static explicit operator HexString(string value)
      //{
      //   return new HexString(value);
      //}

      public static implicit operator byte[](HexString value)
      {
         return value.bytes;
      }

      //public static explicit operator HexString(byte[] value)
      //{
      //   return new HexString(value);
      //}
   }
   
   /// <summary>
   /// Contains unsafe code but it is copied from System.Runtime.Serialization.dll System.Text.BinHexEncoding
   /// </summary>
   internal class BinHexEncoding : Encoding
   {
     // Fields
     private static byte[] char2val = new byte[] { 
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
        0xff, 10, 11, 12, 13, 14, 15, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
        0xff, 10, 11, 12, 13, 14, 15, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 
        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff
     };
     private static string val2char = "0123456789ABCDEF";

     // Methods
     public override int GetByteCount(char[] chars, int index, int count)
     {
       return this.GetMaxByteCount(count);
     }

     [SecurityTreatAsSafe, SecurityCritical]
     public override unsafe int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
     {
       if (chars == null)
         throw new ArgumentNullException("chars");
       if (charIndex < 0)
         throw new ArgumentOutOfRangeException("charIndex", ResourceStrings.GetString("ValueMustBeNonNegative"));
       if (charIndex > chars.Length)
         throw new ArgumentOutOfRangeException("charIndex", ResourceStrings.GetString("OffsetExceedsBufferSize", new object[] { chars.Length }));
       if (charCount < 0)
         throw new ArgumentOutOfRangeException("charCount", ResourceStrings.GetString("ValueMustBeNonNegative"));
       if (charCount > (chars.Length - charIndex))
         throw new ArgumentOutOfRangeException("charCount", ResourceStrings.GetString("SizeExceedsRemainingBufferSpace", new object[] { chars.Length - charIndex }));
       if (bytes == null)
         throw new ArgumentNullException("bytes");
       if (byteIndex < 0)
         throw new ArgumentOutOfRangeException("byteIndex", ResourceStrings.GetString("ValueMustBeNonNegative"));
       if (byteIndex > bytes.Length)
         throw new ArgumentOutOfRangeException("byteIndex", ResourceStrings.GetString("OffsetExceedsBufferSize", new object[] { bytes.Length })); 
       int num = this.GetByteCount(chars, charIndex, charCount);
       if ((num < 0) || (num > (bytes.Length - byteIndex)))
         throw new ArgumentException(ResourceStrings.GetString("XmlArrayTooSmall"), "bytes");
       if (charCount > 0)
       {
         fixed (byte* numRef = char2val)
         {
           fixed (byte* numRef2 = &(bytes[byteIndex]))
           {
             fixed (char* chRef = &(chars[charIndex]))
             {
               char* chPtr = chRef;
               char* chPtr2 = chRef + charCount;
               for (byte* numPtr = numRef2; chPtr < chPtr2; numPtr++)
               {
                 char ch = chPtr[0];
                 char ch2 = chPtr[1];
                 if ((ch | ch2) >= 0x80)
                   throw new FormatException(ResourceStrings.GetString("XmlInvalidBinHexSequence", new object[] { new string(chPtr, 0, 2), charIndex + ((int)((long)((chPtr - chRef) / 2))) }));
                 byte num2 = numRef[(int)((byte)ch)]; // changed byte*
                 byte num3 = numRef[(int)((byte)ch2)]; // changed byte*
                 if ((num2 | num3) == 0xff)
                   throw new FormatException(ResourceStrings.GetString("XmlInvalidBinHexSequence", new object[] { new string(chPtr, 0, 2), charIndex + ((int)((long)((chPtr - chRef) / 2))) }));
                 numPtr[0] = (byte)((num2 << 4) + num3);
                 chPtr += 2;
               }
             }
           }
         }
       }
       return num;
     }

     public override int GetCharCount(byte[] bytes, int index, int count)
     {
       return this.GetMaxCharCount(count);
     }

     [SecurityCritical, SecurityTreatAsSafe]
     public override unsafe int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
     {
       if (bytes == null)
         throw new ArgumentNullException("bytes");
       if (byteIndex < 0)
         throw new ArgumentOutOfRangeException("byteIndex", ResourceStrings.GetString("ValueMustBeNonNegative"));
       if (byteIndex > bytes.Length)
         throw new ArgumentOutOfRangeException("byteIndex", ResourceStrings.GetString("OffsetExceedsBufferSize", new object[] { bytes.Length })); 
       if (byteCount < 0)
         throw new ArgumentOutOfRangeException("byteCount", ResourceStrings.GetString("ValueMustBeNonNegative"));
       if (byteCount > (bytes.Length - byteIndex))
         throw new ArgumentOutOfRangeException("byteCount", ResourceStrings.GetString("SizeExceedsRemainingBufferSpace", new object[] { bytes.Length - byteIndex }));
       int num = this.GetCharCount(bytes, byteIndex, byteCount);
       if (chars == null)
         throw new ArgumentNullException("chars");
       if (charIndex < 0)
         throw new ArgumentOutOfRangeException("charIndex", ResourceStrings.GetString("ValueMustBeNonNegative")); 
       if (charIndex > chars.Length)
         throw new ArgumentOutOfRangeException("charIndex", ResourceStrings.GetString("OffsetExceedsBufferSize", new object[] { chars.Length })); 
       if ((num < 0) || (num > (chars.Length - charIndex)))
         throw new ArgumentException(ResourceStrings.GetString("XmlArrayTooSmall"), "chars"); 
       if (byteCount > 0)
       {
         fixed (char* str = val2char) // ((char*)val2char))
         {
           char* chPtr = str;
           fixed (byte* numRef = &(bytes[byteIndex]))
           {
             fixed (char* chRef = &(chars[charIndex]))
             {
               char* chPtr2 = chRef;
               byte* numPtr = numRef;
               byte* numPtr2 = numRef + byteCount;
               while (numPtr < numPtr2)
               {
                 chPtr2[0] = chPtr[numPtr[0] >> 4];
                 chPtr2[1] = chPtr[numPtr[0] & 15];
                 numPtr++;
                 chPtr2 += 2;
               }
             }
           }
         }
       }
       return num;
     }

     public override int GetMaxByteCount(int charCount)
     {
       if (charCount < 0)
         throw new ArgumentOutOfRangeException("charCount", ResourceStrings.GetString("ValueMustBeNonNegative"));
       if ((charCount % 2) != 0)
         throw new FormatException(ResourceStrings.GetString("XmlInvalidBinHexLength", new object[] { charCount.ToString(NumberFormatInfo.CurrentInfo) }));
       return (charCount / 2);
     }

     public override int GetMaxCharCount(int byteCount)
     {
       if ((byteCount < 0) || (byteCount > 0x3fffffff))
         throw new ArgumentOutOfRangeException("byteCount", ResourceStrings.GetString("ValueMustBeInRange", new object[] { 0, 0x3fffffff })); 
       return (byteCount * 2);
     }
   }

   [Flags]
   public enum HexOptions
   {
      None = 0,
      Default = CaseDefault | EndianLittle | LayoutDefault,
      CaseUpper = 1,
      CaseLower = 0,
      CaseDefault = 0,
      EndianLittle = 2,
      EndianBig = 4,
      // EndianMiddle = EndianBig | EndianLittle, // easy to implement
      EndianDefault = 2,
      Layout8Bit = 0,
      // Layout16Bit = 8, // when this is used we need to append 0x00 sometimes
      // Layout32Bit = 16, // when this is used we need to append 0x000000 somtimes
      LayoutDefault = 0
   }

   public static class ByteExtensions
   {
      /// <summary>
      /// Converts a byte array into a hex string
      /// </summary>      
      public static string ToHex(this byte[] bytes, HexOptions options)
      {
         return new HexString(bytes, options);
      }

      /// <summary>
      /// Converts a byte array into a hex string
      /// </summary>      
      public static string ToHex(this byte[] bytes)
      {
         return new HexString(bytes, HexOptions.Default);
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="bytes"></param>
      /// <returns></returns>
      public static byte[] Reverse(this byte[] bytes)
      {
         var copy = (byte[])bytes.Clone();
         Array.Reverse(copy);
         return copy;
      }

      public static bool IsUpperCase(this HexOptions options)
      {
         return (options & HexOptions.CaseUpper) != HexOptions.None;
      }

      public static bool IsEndianBig(this HexOptions options)
      {
         return (options & HexOptions.EndianBig) != HexOptions.None;
      }

      // byte

      public static byte[] GetBytes(this byte @byte)
      {
         return new byte[] { @byte };
      }

      public static string ToHex(this byte @byte)
      {
         return new HexString(@byte.GetBytes()); ;
      }

      public static string ToHex(this byte @byte, HexOptions options)
      {
         return new HexString(@byte.GetBytes(), options);
      }

      // byte?

      public static byte[] GetBytes(this byte? @byte)
      {
         return @byte.HasValue ? new byte[] { @byte.Value } : null;
      }

      public static string ToHex(this byte? @byte)
      {
         return new HexString(@byte.GetBytes());
      }

      public static string ToHex(this byte? @byte, HexOptions options)
      {
         return new HexString(@byte.GetBytes(), options);
      }

      // sbyte

      public static byte[] GetBytes(this sbyte @sbyte)
      {
         return new byte[] { (byte)@sbyte };
      }

      public static string ToHex(this sbyte @sbyte)
      {
         return new HexString(@sbyte.GetBytes());
      }

      public static string ToHex(this sbyte @sbyte, HexOptions options)
      {
         return new HexString(@sbyte.GetBytes(), options);
      }

      // sbyte?

      public static byte[] GetBytes(this sbyte? @sbyte)
      {
         return @sbyte.HasValue ? @sbyte.Value.GetBytes() : null;
      }

      public static string ToHex(this sbyte? @sbyte)
      {
         return new HexString(@sbyte.GetBytes());
      }

      public static string ToHex(this sbyte? @sbyte, HexOptions options)
      {
         return new HexString(@sbyte.GetBytes(), options);
      }

      // short (Int16)

      public static byte[] GetBytes(this short @short, HexOptions options)
      {
         var buffer = BitConverter.GetBytes(@short);
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this short @short)
      {
         return @short.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this short @short, HexOptions options)
      {
         return new HexString(@short.GetBytes(options), options);
      }

      public static string ToHex(this short @short)
      {
         return new HexString(@short.GetBytes());
      }

      // short?

      public static byte[] GetBytes(this short? @short, HexOptions options)
      {
         var buffer = @short.HasValue ? BitConverter.GetBytes(@short.Value) : null;
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this short? @short)
      {
         return @short.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this short? @short, HexOptions options)
      {
         return new HexString(@short.GetBytes(options), options);
      }

      public static string ToHex(this short? @short)
      {
         return new HexString(@short.GetBytes());
      }

      // ushort (UInt16)

      public static byte[] GetBytes(this ushort @ushort, HexOptions options)
      {
         var buffer = BitConverter.GetBytes(@ushort);
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this ushort @ushort)
      {
         return @ushort.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this ushort @ushort, HexOptions options)
      {
         return new HexString(@ushort.GetBytes(options), options);
      }

      public static string ToHex(this ushort @ushort)
      {
         return new HexString(@ushort.GetBytes());
      }

      // ushort?

      public static byte[] GetBytes(this ushort? @ushort, HexOptions options)
      {
         var buffer = @ushort.HasValue ? BitConverter.GetBytes(@ushort.Value) : null;
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this ushort? @ushort)
      {
         return @ushort.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this ushort? @ushort, HexOptions options)
      {
         return new HexString(@ushort.GetBytes(options), options);
      }

      public static string ToHex(this ushort? @ushort)
      {
         return new HexString(@ushort.GetBytes());
      }

      // int (Int32)

      public static byte[] GetBytes(this int @int, HexOptions options)
      {
         var buffer = BitConverter.GetBytes(@int);
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this int @int)
      {
         return @int.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this int @int, HexOptions options)
      {
         return new HexString(@int.GetBytes(options), options);
      }

      public static string ToHex(this int @int)
      {
         return new HexString(@int.GetBytes());
      }

      // int?

      public static byte[] GetBytes(this int? @int, HexOptions options)
      {
         var buffer = @int.HasValue ? BitConverter.GetBytes(@int.Value) : null;
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this int? @int)
      {
         return @int.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this int? @int, HexOptions options)
      {
         return new HexString(@int.GetBytes(options), options);
      }

      public static string ToHex(this int? @int)
      {
         return new HexString(@int.GetBytes());
      }

      // uint (UInt32)

      public static byte[] GetBytes(this uint @uint, HexOptions options)
      {
         var buffer = BitConverter.GetBytes(@uint);
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this uint @uint)
      {
         return @uint.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this uint @uint, HexOptions options)
      {
         return new HexString(@uint.GetBytes(options), options);
      }

      public static string ToHex(this uint @uint)
      {
         return new HexString(@uint.GetBytes());
      }

      // uint?

      public static byte[] GetBytes(this uint? @uint, HexOptions options)
      {
         var buffer = @uint.HasValue ? BitConverter.GetBytes(@uint.Value) : null;
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this uint? @uint)
      {
         return @uint.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this uint? @uint, HexOptions options)
      {
         return new HexString(@uint.GetBytes(options), options);
      }

      public static string ToHex(this uint? @uint)
      {
         return new HexString(@uint.GetBytes());
      }

      // long (Int64)

      public static byte[] GetBytes(this long @long, HexOptions options)
      {
         var buffer = BitConverter.GetBytes(@long);
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this long @long)
      {
         return @long.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this long @long, HexOptions options)
      {
         return new HexString(@long.GetBytes(options), options);
      }

      public static string ToHex(this long @long)
      {
         return new HexString(@long.GetBytes());
      }

      // long?

      public static byte[] GetBytes(this long? @long, HexOptions options)
      {
         var buffer = @long.HasValue ? BitConverter.GetBytes(@long.Value) : null;
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this long? @long)
      {
         return @long.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this long? @long, HexOptions options)
      {
         return new HexString(@long.GetBytes(options), options);
      }

      public static string ToHex(this long? @long)
      {
         return new HexString(@long.GetBytes());
      }

      // ulong (UInt64)

      public static byte[] GetBytes(this ulong @ulong, HexOptions options)
      {
         var buffer = BitConverter.GetBytes(@ulong);
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this ulong @ulong)
      {
         return @ulong.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this ulong @ulong, HexOptions options)
      {
         return new HexString(@ulong.GetBytes(options), options);
      }

      public static string ToHex(this ulong @ulong)
      {
         return new HexString(@ulong.GetBytes());
      }

      // ulong?

      public static byte[] GetBytes(this ulong? @ulong, HexOptions options)
      {
         var buffer = @ulong.HasValue ? BitConverter.GetBytes(@ulong.Value) : null;
         if (options.IsEndianBig())
            Array.Reverse(buffer);
         return buffer;
      }

      public static byte[] GetBytes(this ulong? @ulong)
      {
         return @ulong.GetBytes(HexOptions.Default);
      }

      public static string ToHex(this ulong? @ulong, ulong defaultValue, HexOptions options)
      {
         return new HexString((@ulong ?? defaultValue).GetBytes(options), options);
      }

      public static string ToHex(this ulong? @ulong, HexOptions options)
      {
         return new HexString(@ulong.GetBytes(options), options);
      }

      public static string ToHex(this ulong? @ulong, ulong defaultValue)
      {
         return new HexString((@ulong ?? defaultValue).GetBytes());
      }

      public static string ToHex(this ulong? @ulong)
      {
         return new HexString(@ulong.GetBytes());
      }

      // utility class

      public class BytesBuilder
      {
         private StringBuilder builder;
         private static BinHexEncoding coder;

         static BytesBuilder()
         {
            coder = new BinHexEncoding();
         }

         public HexOptions Options;

         #region Constructors

         // constructors accepting a string?

         public BytesBuilder()
         {
            builder = new StringBuilder();
         }

         public BytesBuilder(int capacity)
         {
            builder = new StringBuilder(capacity * 2);
         }

         public BytesBuilder(byte[] value)
         {
            builder = new StringBuilder(value.ToHex());
         }

         public BytesBuilder(byte[] value, int startIndex, int length, int capacity)
         {
            builder = new StringBuilder(value.ToHex(), startIndex * 2, length * 2, capacity * 2);
         }

         public BytesBuilder(int capacity, int maxCapacity)
         {
            builder = new StringBuilder(capacity * 2, maxCapacity * 2);
         }

         public BytesBuilder(byte[] value, int capacity)
         {
            builder = new StringBuilder(value.ToHex(), capacity * 2);
         }

         #endregion

         #region AppendXXX (general)

         public void AppendBytes(byte[] value)
         {
            builder.Append(value.ToHex());
         }

         public void AppendHexString(string source)
         {
            // only do conversion to check contents
            builder.Append(coder.GetBytes(source));
         }

         #endregion

         #region AppendXXX (non-nullable)

         public void AppendByte(byte value)
         {
            builder.Append(value.ToHex());
         }

         public void AppendSByte(sbyte value)
         {
            builder.Append(value.ToHex());
         }

         public void AppendInt16(short value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendUInt16(ushort value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendInt32(int value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendUInt32(uint value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendInt64(long value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendUInt64(ulong value)
         {
            builder.Append(value.ToHex(Options));
         }

         #endregion

         #region AppendXXX (nullable)

         public void AppendByte(byte? value)
         {
            builder.Append(value.ToHex());
         }

         public void AppendSByte(sbyte? value)
         {
            builder.Append(value.ToHex());
         }

         public void AppendInt16(short? value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendUInt16(ushort? value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendInt32(int? value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendUInt32(uint? value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendInt64(long? value)
         {
            builder.Append(value.ToHex(Options));
         }

         public void AppendUInt64(ulong? value)
         {
            builder.Append(value.ToHex(Options));
         }

         #endregion

         #region InsertXXX (general)

         public void InsertBytes(int index, byte[] value)
         {
            builder.Insert(index * 2, value.ToHex());
         }

         public void InsertHexString(int index, string source)
         {
            builder.Insert(index * 2, coder.GetBytes(source));
         }

         #endregion

         #region InsertXXX (non-nullable)

         public void InsertByte(int index, byte value)
         {
            builder.Insert(index, value.ToHex());
         }

         public void InsertSByte(int index, sbyte value)
         {
            builder.Insert(index, value.ToHex());
         }

         public void InsertInt16(int index, short value)
         {
            builder.Insert(index, value.ToHex(Options));
         }

         public void InsertUInt16(int index, ushort value)
         {
            builder.Insert(index, value.ToHex(Options));
         }

         public void InsertInt32(int index, int value)
         {
            builder.Insert(index, value.ToHex(Options));
         }

         public void InsertUInt32(int index, uint value)
         {
            builder.Insert(index, value.ToHex(Options));
         }

         public void InsertInt64(int index, long value)
         {
            builder.Insert(index, value.ToHex(Options));
         }

         public void InsertUInt64(int index, ulong value)
         {
            builder.Insert(index, value.ToHex(Options));
         }

         #endregion 

         #region Result conversions

         public string ToString()
         {
            return builder.ToString();
         }

         public byte[] ToBytes()
         {
            return new HexString(builder.ToString());
         }

         #endregion

         public void Test()
         {
           ulong? i = 93485;
         }
         // Remove
         // Insert
         // EnsureCapacity
         // ToBytes()
         // CopyTo?
      }
   }
}
